﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Configuration;
using System.Drawing.Design;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Permissions;
using System.Web;
using System.Web.Configuration;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using Moxiecode.TinyMCE.Settings;
using Moxiecode.TinyMCE.Configuration;
using Moxiecode.TinyMCE.Web.Controls.Design;

namespace Moxiecode.TinyMCE.Web.Controls
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [ParseChildren(true), PersistChildren(false)]
    [Designer(typeof(TinyMCEDesigner))]
    [Themeable(true)]
    public abstract class BaseTinyMCE : Control
    {
        #region Constructors

        public BaseTinyMCE() : base()
        {
            LoadConfigSettings(ConfigurationManager.GetSection("TinyMCE") as TinyConfigSection);
            OnGlobalSettingsAssigned(new EventArgs());
        }

        #endregion

        #region Properties

        #region TinyMCE Reference Architecture

        private string _installPath;
        [Category("TinyMCE Reference"), Description("The path of the directory where the TinyMCE scripts are stored.")]
        [DefaultValue("~/js/tiny_mce/")]
        public string InstallPath
        {
            get { return _installPath; }
            set
            {
                _installPath = String.IsNullOrEmpty(value) ? null : VirtualPathUtility.AppendTrailingSlash(value);
            }
        }

        private string _fileName;
        [Category("TinyMCE Reference"), Description("Specifies the name of the TinyMCE file to use for processing.")]
        [DefaultValue("tiny_mce")]
        public string FileName
        {
            get { return _fileName; }
            set { _fileName = value; }
        }

        private string _suffix = String.Empty;
        [Category("TinyMCE Reference"), Description("Allows for referencing different versions of a file (for example, 'src')")]
        [DefaultValue("")]
        public string Suffix
        {
            get { return _suffix; }
            set { _suffix = value; }
        }

        protected string FilePath
        {
            get
            {
                string name = VirtualPathUtility.AppendTrailingSlash(InstallPath) + FileName;
                return String.IsNullOrEmpty(Suffix) ? name + ".js" : name + "_" + Suffix + ".js";
            }
        }

        #region Include Architecture

        protected virtual string IncludeUrl
        {
            get { return (Gzip.Enabled) ? GzipInclude : StandardInclude; }
        }

        protected abstract string GzipInclude { get; }

        protected virtual string StandardInclude
        {
            get 
            {
                if (this.InstallPath == null)
                    throw new Exception(Localization_en.InvalidInstallPath);

                string uri = FilePath;
                string local = HttpContext.Current.Server.MapPath(uri);
                if (!File.Exists(local))
                    throw new Exception(String.Format(Localization_en.TinyNotFound, local));
                return uri;
            }
        }

        #endregion

        #endregion

        #region Target Architecture

        private List<Target> _targets;
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Category("Selection"), Description("The target TextBox controls which are referenced in Exact mode")]
        public List<Target> Targets
        {
            get { return _targets; }
            private set { _targets = value; }
        }

        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
        protected Control[] TargetControls
        {
            get 
            {
                // Adding elements to extend
                if (Mode == Mode.Exact)
                    return (Targets.ConvertAll<Control>(t => FindControl(t.TargetID)).Where(c => c != null)).ToArray();
                else
                {
                    Control[] exactTargets = FindControls<BaseTinyMCE>().Where(t => t.Mode == Mode.Exact).SelectMany(t => t.TargetControls).ToArray();
                    return FindControls<TextBox>().Where(t => !exactTargets.Contains(t)).ToArray();
                }
            }
        }

        protected string[] TargetClientIDs
        {
            get { return (TargetControls.Select(c => c.ClientID)).ToArray(); }
        }

        #endregion

        #region Gzip Architecture

        private GzipSettings _gzip;
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [Category("Configuration"), Description("The settings used for Gzip compression of TinyMCE files.")]
        public GzipSettings Gzip
        {
            get { return _gzip; }
            set { _gzip = value; }
        }

        #endregion

        #region Settings Architecture

        private Setting[] _settings;
        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
        public virtual Setting[] Settings
        {
            get
            {
                if (_settings == null)
                {
                    _settings = (from p in this.GetType().GetProperties()
                                 where Setting.IsSetting(p)
                                 select (new Setting(p, this))).ToArray();
                }
                return _settings;
            }
        }

        protected virtual string[] SettingsCommands
        {
            get 
            { 
                List<string> setLines = new List<string>();

                ValidateTargets();

                // Adding elements to extend
                setLines.Add(String.Format(Localization_en.StringCommandFormat, "elements", String.Join(",", TargetClientIDs)));

                // if there are elements to extend, continue
                if (setLines.Count > 0)
                {
                    setLines.Add(String.Format(Localization_en.StringCommandFormat, "mode", "exact"));
                    if (EncodeDecodeHtml)
                    {
                        setLines.Add(String.Format(Localization_en.StringCommandFormat, "cleanup_callback", "cleanupCallback"));
                        setLines.Add(String.Format(Localization_en.StringCommandFormat, "save_callback", "saveCallback"));
                    }
                }
                return setLines.ToArray();
            }
        }

        #endregion

        #region General

        private Mode _mode;
        [Category("Selection"), Description("Selection method for choosing which items will become TinyMCE editors")]
        [DefaultValue(Mode.TextBoxes)]
        public Mode Mode
        {
            get { return _mode; }
            set { _mode = value; }
        }

        private bool _encodeDecodeHtml = true;
        [Category("Configuration"), Description("When requests are validated (the default in ASP.NET), an error will be trown. If this setting is true, the html will be encoded and then decoded to avoid this error.")]
        [DefaultValue(true)]
        public bool EncodeDecodeHtml
        {
            get { return _encodeDecodeHtml; }
            set { _encodeDecodeHtml = value; }
        }

        [Category("Layout"), Description("The width of all targets.")]
        public Unit Width { get; set; }

        [Category("Layout"), Description("The height of all targets.")]
        public Unit Height { get; set; }

        [Browsable(true)]
        public override string SkinID
        {
            get { return base.SkinID; }
            set { base.SkinID = value; }
        }

        private ExtensionMode _extensionMode = ExtensionMode.Default;
        [Category("Behavior"), Description("Specifies whether to extend parent, child and/or sibling controls.")]
        [DefaultValue(ExtensionMode.Default)]
        public ExtensionMode ExtensionMode
        {
            get { return _extensionMode; }
            set { _extensionMode = value; }
        }

        #endregion

        #endregion

        #region Events

        public event EventHandler GlobalSettingsAssigned;
        public virtual void OnGlobalSettingsAssigned(EventArgs e)
        {
            CallEvent(GlobalSettingsAssigned, e);
        }

        public event EventHandler PreRenderInclude;
        public virtual void OnPreRenderInclude(EventArgs e)
        {
            CallEvent(PreRenderInclude, e);
        }

        public event EventHandler RenderIncludeComplete;
        public virtual void OnRenderIncludeComplete(EventArgs e)
        {
            CallEvent(RenderIncludeComplete, e);
        }

        public event EventHandler PreRenderEncoding;
        public virtual void OnPreRenderEncoding(EventArgs e)
        {
            CallEvent(PreRenderEncoding, e);
        }

        public event EventHandler RenderEncodingComplete;
        public virtual void OnRenderEncodingComplete(EventArgs e)
        {
            CallEvent(RenderEncodingComplete, e);
        }

        public event EventHandler PreRenderScript;
        public virtual void OnPreRenderScript(EventArgs e)
        {
            CallEvent(PreRenderScript, e);
        }

        public event EventHandler RenderScriptComplete;
        public virtual void OnRenderScriptComplete(EventArgs e)
        {
            CallEvent(RenderScriptComplete, e);
        }

        public event EventHandler PreHtmlDecode;
        public virtual void OnPreHtmlDecode(EventArgs e)
        {
            CallEvent(PreHtmlDecode, e);
        }

        public event EventHandler HtmlDecodeComplete;
        public virtual void OnHtmlDecodeComplete(EventArgs e)
        {
            CallEvent(HtmlDecodeComplete, e);
        }

        private void CallEvent(EventHandler handler, EventArgs e)
        {
            if (handler != null)
            {
                if (e == null) e = EventArgs.Empty;
                handler(this, e);
            }
        }

        #endregion

        #region Methods

        #region Initialization

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            if (Page != null)
                Page.PreLoad += new EventHandler(this.Page_PreLoad);
        }

        #region Decoding Html

        protected void Page_PreLoad(object sender, EventArgs e)
        {
            if (EncodeDecodeHtml && Page.IsPostBack)
            {
                OnPreHtmlDecode(new EventArgs());
                bool b = Decode();
                OnHtmlDecodeComplete(new EventArgs());
            }
        }

        protected virtual bool Decode()
        {
            foreach (Control c in TargetControls)
            {
                if (c is TextBox)
                {
                    TextBox b = (TextBox)c;
                    b.Text = b.Text.Replace("&gt;", ">").Replace("&lt;", "<").Replace("&amp;", "&");
                }
            }
            return true;
        }

        #endregion

        #endregion

        #region Rendering

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            PrepRenderState();
            if (TargetControls.Length > 0)
            {
                OnPreRenderInclude(new EventArgs());
                RenderInclude();
                OnRenderIncludeComplete(new EventArgs());
            }
        }

        protected virtual void PrepRenderState() { }

        protected override void Render(HtmlTextWriter outWriter)
        {
            if (TargetControls.Length == 0) return;
            if (EncodeDecodeHtml)
            {
                OnPreRenderEncoding(new EventArgs());
                RenderEncoding();
                OnRenderEncodingComplete(new EventArgs());
            }
            OnPreRenderScript(new EventArgs());
            RenderScript();
            OnRenderScriptComplete(new EventArgs());

            TargetControls.OfType<TextBox>().ToList().ForEach(AlterTarget);
        }

        protected virtual void RenderInclude()
        {
            string uri = ResolveClientUrl(IncludeUrl);
            if (!Page.ClientScript.IsClientScriptBlockRegistered(uri))
                Page.ClientScript.RegisterClientScriptBlock(Page.GetType(), uri, "<script type=\"text/javascript\" src=\"" + uri + "\"></script>");
        }

        protected void RenderEncoding()
        {
            System.Text.StringBuilder builder = new System.Text.StringBuilder();
            builder.AppendLine("<script type=\"text/javascript\">");
            builder.AppendLine("function cleanupCallback(type, value)");
            builder.AppendLine("{");
            builder.AppendLine("\tif (type == \"insert_to_editor\")");
            builder.AppendLine("\t{");   
            builder.AppendLine("\t\tvalue = value.replace(/&lt;/gi,\"<\");");
            builder.AppendLine("\t\tvalue = value.replace(/&gt;/gi,\">\");");
            builder.AppendLine("\t}");
            builder.AppendLine("\treturn value;");
            builder.AppendLine("}");
            builder.AppendLine("function saveCallback(element_id, html, body)");
            builder.AppendLine("{");
            builder.AppendLine("\thtml = html.replace(/&/gi, \"&amp;\");");
            builder.AppendLine("\thtml = html.replace(/</gi,\"&lt;\");");
            builder.AppendLine("\thtml = html.replace(/>/gi,\"&gt;\");");
            builder.AppendLine("\treturn html;");
            builder.AppendLine("}");
            builder.AppendLine("</script>");

            if(!Page.ClientScript.IsStartupScriptRegistered("encoding"))
                Page.ClientScript.RegisterStartupScript(Page.GetType(), "encoding", builder.ToString(), false);
        }

        protected virtual void RenderScript()
        {
            System.Text.StringBuilder builder = new System.Text.StringBuilder();
            builder.AppendLine("<script type=\"text/javascript\">");
            builder.AppendLine("tinyMCE.init({");
            builder.Append("\t");
            builder.AppendLine(String.Join(",\n\t", SettingsCommands));
            builder.AppendLine("});");
            builder.AppendLine("</script>");

            Page.ClientScript.RegisterStartupScript(Page.GetType(), ClientID, builder.ToString(), false);
        }

        protected virtual void ValidateTargets()
        {
            Control[] controls = TargetControls;
            foreach (Target t in Targets)
            {
                if (controls.FirstOrDefault(c => c.ID == t.TargetID) == null)
                    throw new Exception("Could not find control: " + t.TargetID + ".");
            }

            if (Mode == Mode.Exact)
            {
                var nonTextBoxes = controls.Where(c => !(c is TextBox));
                if (nonTextBoxes.Count() > 0)
                    throw new Exception("The following controls are not TextBox controls: " + String.Join(", ", nonTextBoxes.Select(c => c.ID).ToArray()) + ".");
            }
        }

        protected virtual void AlterTarget(TextBox target)
        {
            if (Width != null) target.Width = Width;
            if (Height != null) target.Height = Height;
        }

        #endregion

        #region Overridden

        protected sealed override void CreateChildControls() { Controls.Clear(); }

        #region FindControl

        private Dictionary<string, Control> _controlCache = new Dictionary<string, Control>();

        public override Control FindControl(string id)
        {
            // If cached, return cached copy
            if (_controlCache.ContainsKey(id)) return _controlCache[id];

            // Search inner and outer NamingContainers
            Control control = base.FindControl(id);
            if(control == null && (ExtensionMode == ExtensionMode.Up || ExtensionMode == ExtensionMode.All)) 
                control = FindOuterControl(id);
            
            if (control == null && (ExtensionMode == ExtensionMode.Down || ExtensionMode == ExtensionMode.All)) 
                control = FindInnerControl(NamingContainer, id);

            // Cache if found, then return
            if (control != null) _controlCache[id] = control;
            return control;
        }

        private Control FindOuterControl(string id)
        {
            if (NamingContainer == null) return null;
            Control control = NamingContainer.FindControl(id);
            Control container = NamingContainer.NamingContainer;
            while ((control == null) && (container != null))
            {
                if (container == this) continue;
                control = container.FindControl(id);
                if (ExtensionMode == ExtensionMode.All)
                {
                    List<Control> controls = new List<Control>();
                    container.Controls.OfType<Control>().Where(c => c.Controls.Count > 0).ToList().ForEach(c => controls.Add(FindInnerControl(c, id)));
                    if (controls.Count > 0) control = controls.First();
                }
                container = container.NamingContainer;
            }
            return control;
        }

        private Control FindInnerControl(Control c, string id)
        {
            if (c.ID == id) return c;
            foreach (Control ctl in c.Controls)
            {
                Control result = FindInnerControl(ctl, id);
                if (result != null) return result;
            }
            return null;
        }

        #endregion

        #endregion

        #region Find Controls

        protected virtual T[] FindControls<T>()
        {
            if (NamingContainer == null) return new T[0];

            // Search siblings
            IEnumerable<Control> siblings = NamingContainer.Controls.OfType<Control>().Where(c => c != this);
            List<T> controls = siblings.OfType<T>().ToList();

            // Search parents (and older) by passing the grandparent
            if(ExtensionMode == ExtensionMode.Up || ExtensionMode == ExtensionMode.All)
                controls.AddRange(FindOuterControls<T>(NamingContainer.NamingContainer));

            // Search siblings children
            if(ExtensionMode == ExtensionMode.Down || ExtensionMode == ExtensionMode.All)
                siblings.ToList().ForEach(c => controls.AddRange(FindInnerControls<T>(c)));
            
            return controls.ToArray();
        }

        private List<T> FindOuterControls<T>(Control container)
        {
            if (container == null) return new List<T>();
            List<T> controls = container.Controls.OfType<T>().ToList();
            container = container.NamingContainer;
            while (container != null)
            {
                controls.AddRange(container.Controls.OfType<T>());
                if (ExtensionMode == ExtensionMode.All)
                    container.Controls.OfType<Control>().Where(c => c.Controls.Count > 0).ToList().ForEach(c => controls.AddRange(FindInnerControls<T>(c)));
                container = container.NamingContainer;
            }
            return controls;
        }

        private List<T> FindInnerControls<T>(Control container)
        {
            List<T> controls = new List<T>(container.Controls.OfType<T>().Cast<T>());
            container.Controls.OfType<Control>().ToList().ForEach(c => controls.AddRange(FindInnerControls<T>(c)));
            return controls;
        }

        #endregion

        #region Global Configuration Integration

        protected virtual void LoadConfigSettings(TinyConfigSection config)
        {
            if (config == null) config = new TinyConfigSection();
            InstallPath = config.InstallPath;
            Mode = config.Mode;
            FileName = config.FileName;
            Suffix = config.Suffix;
            Gzip = new GzipSettings(config.GzipCompressor);
            Targets = new List<Target>();

            foreach (KeyValueConfigurationElement item in config.GlogalSettings)
            {
                PropertyInfo prop = Array.Find<PropertyInfo>(this.GetType().GetProperties(), i => i.Name == item.Key);
                if (prop != null && prop.CanWrite)
                {
                    try
                    {
                        TypeConverter converter = TypeDescriptor.GetConverter(prop.PropertyType);
                        if (converter.CanConvertFrom(typeof(string)))
                            prop.SetValue(this, converter.ConvertFrom(item.Value), null);
                    }
                    catch { /* TODO: Log attempted property assignments? */ }
                }
            }
        }

        #endregion

        #endregion
    }
}
